package org.sand.common.util;

import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

/**
 * @Description: TODO用于执行命令工具类
 * @author: 929773669@qq.com
 * @date: 2024年08月29日
 */
@Slf4j
public class ProcessExecutor {
    private final Semaphore semaphore;
    private final boolean liveUpdates;
    private long timeoutDuration;//分钟

    ExecutorService fixedThreadPool = Executors.newFixedThreadPool(15);

    public ProcessExecutor(int semaphoreLimit, boolean liveUpdates,long timeout) {
        this.semaphore = new Semaphore(semaphoreLimit);
        this.liveUpdates = liveUpdates;
        this.timeoutDuration = timeout;
    }

    public ProcessExecutor(){
         this(1,false,1);
    }

    public ProcessExecutorResult runCommandWithOutputHandling(List<String> command)
            throws IOException, InterruptedException {
        return runCommandWithOutputHandling(command, null);
    }

    public ProcessExecutorResult runCommandWithOut(List<String> command)
            throws IOException, InterruptedException {
        return runCommandWithOut(command, null);
    }

    public void runCommand(List<String> command,Boolean isOut) throws Exception {
        log.info("Running command: " + String.join(" ", command));
        ProcessBuilder processBuilder = new ProcessBuilder(command);
        Process process = processBuilder.start();// 读取进程输出
        if(isOut) {
            StreamGobbler outputGobbler = new StreamGobbler(process.getInputStream(), "OUTPUT");
            StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), "ERROR");

            // 启动线程来读取输出和错误流
            outputGobbler.start();
            errorGobbler.start();

            fixedThreadPool.submit(()->{
                try {
                    process.waitFor();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    log.error(e.getMessage());
                }
            });
        }


    };

    public ProcessExecutorResult runCommandWithOutputHandling(List<String> command, File workingDirectory) throws IOException, InterruptedException {
        String messages = "";
        int exitCode = 1;
        semaphore.acquire();
        try {

            log.info("Running command: " + String.join(" ", command));
            ProcessBuilder processBuilder = new ProcessBuilder(command);

            // Use the working directory if it's set
            if (workingDirectory != null) {
                processBuilder.directory(workingDirectory);
            }
            Process process = processBuilder.start();

            // Read the error stream and standard output stream concurrently
            List<String> errorLines = new ArrayList<>();
            List<String> outputLines = new ArrayList<>();

            Thread errorReaderThread =
                    new Thread(
                            () -> {
                                try (BufferedReader errorReader =
                                             new BufferedReader(
                                                     new InputStreamReader(
                                                             process.getErrorStream(),
                                                             StandardCharsets.UTF_8))) {
                                    String line;
                                    while ((line =errorReader.readLine())
                                            != null) {
                                        errorLines.add(line);
                                        if (liveUpdates) log.info(line);
                                    }
                                } catch (InterruptedIOException e) {
                                    log.warn("Error reader thread was interrupted due to timeout.");
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            });

            Thread outputReaderThread =
                    new Thread(
                            () -> {
                                try (BufferedReader outputReader =
                                             new BufferedReader(
                                                     new InputStreamReader(
                                                             process.getInputStream(),
                                                             StandardCharsets.UTF_8))) {
                                    String line;
                                    while ((line =outputReader.readLine())!= null) {
                                        outputLines.add(line);
                                        if (liveUpdates) log.info(line);
                                    }
                                } catch (InterruptedIOException e) {
                                    log.warn("Error reader thread was interrupted due to timeout.");
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            });

            errorReaderThread.start();
            outputReaderThread.start();

            // Wait for the conversion process to complete
            boolean finished = process.waitFor(timeoutDuration, TimeUnit.MINUTES);

            if (!finished) {
                // Terminate the process
                process.destroy();
                // Interrupt the reader threads
                errorReaderThread.interrupt();
                outputReaderThread.interrupt();
                throw new IOException("Process timeout exceeded.");
            }
            exitCode = process.exitValue();
            // Wait for the reader threads to finish
            errorReaderThread.join();
            outputReaderThread.join();

            if (!liveUpdates) {
                if (outputLines.size() > 0) {
                    String outputMessage = String.join("\n", outputLines);
                    messages += outputMessage;
                    log.info("Command output:\n" + outputMessage);
                }

                if (errorLines.size() > 0) {
                    String errorMessage = String.join("\n", errorLines);
                    messages += errorMessage;
                    log.warn("Command error output:\n" + errorMessage);
                    if (exitCode != 0) {
                        throw new IOException("Command process failed with exit code " + exitCode + ". Error message: " + errorMessage);
                    }
                }
            } else if (exitCode != 0) {
                throw new IOException("Command process failed with exit code " + exitCode);
            }
        } finally {
            semaphore.release();
        }
        return new ProcessExecutorResult(exitCode, messages);
    }

    public ProcessExecutorResult runCommandWithOut(List<String> command, File workingDirectory) throws IOException, InterruptedException {
        String messages = "";
        int exitCode = 1;
        List<String> outputLines = new ArrayList<>();
        semaphore.acquire();
        try {
            log.info("Running command: " + String.join(" ", command));
            ProcessBuilder processBuilder = new ProcessBuilder(command);
            // Use the working directory if it's set
            if (workingDirectory != null) {
                processBuilder.directory(workingDirectory);
            }
            Process process = processBuilder.start();
            // Read the error stream and standard output stream concurrently
            List<String> errorLines = new ArrayList<>();
            Thread errorReaderThread =
                    new Thread(() -> {
                                try (BufferedReader errorReader =
                                             new BufferedReader(
                                                     new InputStreamReader(
                                                             process.getErrorStream(),
                                                             StandardCharsets.UTF_8))) {
                                    String line;
                                    while ((line =errorReader.readLine())
                                            != null) {
                                        errorLines.add(line);
                                        if (liveUpdates) log.info(line);
                                    }
                                } catch (InterruptedIOException e) {
                                    log.warn("Error reader thread was interrupted due to timeout.");
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            });
            errorReaderThread.start();

            // Wait for the conversion process to complete
            boolean finished = process.waitFor(timeoutDuration, TimeUnit.MINUTES);

            if (!finished) {
                // Terminate the process
                process.destroy();
                // Interrupt the reader threads
                errorReaderThread.interrupt();
                throw new IOException("Process timeout exceeded.");
            }
            exitCode = process.exitValue();
            // Wait for the reader threads to finish
            errorReaderThread.join();
            try (BufferedReader outputReader =new BufferedReader(new InputStreamReader(process.getInputStream(),StandardCharsets.UTF_8))) {
                String line;
                while ((line =outputReader.readLine())!= null) {
                    outputLines.add(line);
                    if (liveUpdates) log.info(line);
                }
            } catch (InterruptedIOException e) {
                log.warn("Error reader thread was interrupted due to timeout.");
            }

            if (!liveUpdates) {
                if (outputLines.size() > 0) {
                    String outputMessage = String.join("\n", outputLines);
                    messages += outputMessage;
                    log.info("Command output:\n" + outputMessage);
                }

                if (errorLines.size() > 0) {
                    String errorMessage = String.join("\n", errorLines);
                    messages += errorMessage;
                    log.warn("Command error output:\n" + errorMessage);
                    if (exitCode != 0) {
                        throw new IOException("Command process failed with exit code " + exitCode + ". Error message: " + errorMessage);
                    }
                }
            } else if (exitCode != 0) {
                throw new IOException("Command process failed with exit code " + exitCode);
            }
        } finally {
            semaphore.release();
        }
        return new ProcessExecutorResult(exitCode, messages,outputLines);
    }


    public class ProcessExecutorResult {
        int rc;
        String messages;
        Object data;

        public ProcessExecutorResult(int rc, String messages) {
            this.rc = rc;
            this.messages = messages;
        }
        public ProcessExecutorResult(int rc, String messages, Object data) {
            this.rc = rc;
            this.messages = messages;
            this.data = data;
        }

        public int getRc() {
            return rc;
        }

        public void setRc(int rc) {
            this.rc = rc;
        }

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public String getMessages() {
            return messages;
        }

        public void setMessages(String messages) {
            this.messages = messages;
        }
    }

    class StreamGobbler extends Thread {
        private InputStream inputStream;
        private String type;

        public StreamGobbler(InputStream inputStream, String type) {
            this.inputStream = inputStream;
            this.type = type;
        }

        @Override
        public void run() {
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    log.info(type + " > " + line);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
